the life of every design system, there comes a time when something needs to change. Analytics can show an underused icon not worth maintaining, or user research could suggest a redesign of your navigation component.|
Working on Encore, Spotify’s internal design system, we are constantly seeking this type of opportunity to refine and improve our system. But responding to these opportunities may mean changing or removing something our consumers rely on. This is called a breaking change.
A breaking change is a modification to the system that has the potential to break downstream applications.
Use Semantic Versioning
Today, most libraries follow a version-naming system called SemVer (or semantic versioning) built around the concept of breaking changes. Each version is assigned with 3 numbers: major, minor, and patch.
A breaking change is released in a new major version. The first (major) number is increased by 1, and the second (minor) and third (patch) numbers are reset to 0 (e.g. version 2.8.3 => 3.0.0). This means a consumer on version 2.8.3 can upgrade to any subsequent 2.X.X version without fear of breaking their product, and without needing to plan a migration or extensive testing.
These version numbers are a contract between your design system and its consumers. This contract is the cornerstone of building and maintaining trust in your system. The catch: there is no single, global definition of a breaking change. You must write this definition yourself.
Eliminating a feature of your API is a clear breaking change, but a subtle visual tweak may be low-risk enough to be included in a minor version. As a design system maintainer, that is up to you to decide. Regardless of what you choose, socialize your definition of a breaking change within your organization so that everyone is on the same page.
Change is hard
Successful change management is no small feat. Second only to naming things, I have found change management to be the hardest part of being a design system engineer — much more complex than any technical challenge. The stakes are high. Depending on how they are handled, breaking changes can either propel your system into the future or bring an end to consumer adoption.
Over the years, I’ve been on both sides of some thorny breaking changes (and plenty of successful ones too). Here are some key tips for how you can make a break for it without leaving your consumers in the dust.
Identify “key consumers”
To understand how a change will impact your consumers, you must know who those consumers are. As system adoption grows, knowing the ins and outs of every consuming application isn’t scalable, and analytics can only get you so far. Enter “key consumers”.
Key consumers, much like target personas, are a representative subset of consumer teams or products that your design system can use as case studies when making big decisions.
Pick a varied subset of users
Choose your key consumers by selecting a manageable number of teams, products, or even individuals that use your design system. Make this group as varied as possible. Challenge yourself by including at least one “aspirational” consumer that isn’t a major adopter yet, but has the potential to be.
At Spotify, our web design system team has identified a few key consuming products that we take into special consideration before finalizing major impactful decisions. One product is important to us because they have a long history of high adoption. Another consumer is equally important for exactly the opposite reason — they are new users of the design system and have relatively low adoption.
Choosing key consumers with diverse application complexities, organizational structures, user bases, and technical and design requirements allows us to stress-test our choices and be confident of the impact those decisions will have.
Embed your way to a strong relationship
It’s a good idea to establish a close working relationship with your key consumers. The best way I’ve found to achieve this is by walking a few sprints in their shoes. If you are on a dedicated systems team, consider switching to work full-time in a feature team for a month or two. At Spotify, we call this “embedding”. Embedding is a gesture of goodwill that’s mutually beneficial — both teams gain empathy and technical understanding, and the feature team gets an extra pair of hands for a while.
Scheduling an embed when a consumer is planning a design system-related migration is especially beneficial. This allows you to stress-test your migration guide and get upgrade experience from a consumer’s point-of-view, while also helping to drive new version adoption.
Takeaway: Focus on a representative sample of consumers and establish a close working relationship with them. In exchange for their participation in beta testing, embed in their teams to assist with migrations and other work.
Justify the changes
Breaking changes are not something to be scared or ashamed of. They are signs that your design system is maturing and adapting to new use cases. But don’t be surprised if not all consumers view them in such a positive light. Many people (engineers in particular, in my personal experience) are bound to be curious, and even skeptical, about your reasons. It’s only fair — if you make a change that requires extra effort on their part, they have a right to know, and challenge, that reason.
Explain the problems with persisting the old way of doing things, and describe how this change will benefit them in the long run. Show the options you weighed before making this decision, and why you chose the decision you did. Sharing this level of openness builds trust and may help break down the “us vs. them” mentality that can divide design system maintainers and consumers, even when both parties are part of the same company and have similar goals.
Author an RFC
Writing an RFC, or “Request for Comments”, creates an ideal format for this sort of dialog. This document outlines your proposed changes, the various options you weighed, and why you chose the option you did. Traditionally, RFCs are open for a certain length of time (usually a few weeks), during which everyone is encouraged to comment on the document with criticism, approval, and alternative ideas. When an RFC closes, a final decision is written up in an outcome statement and is socialized.
“A Request for Comments (RFC) is a formal document drafted by the Internet Engineering Task Force (IETF) that describes the specifications for a particular technology. When an RFC is ratified, it becomes a formal standards document.”
Using the RFC process to propose breaking changes not only gives your consumers a heads-up about upcoming changes, but also makes them active participants in the decision process. By the time a final decision is reached, your consumers have had the chance to ask questions, air grievances, and flag potential problems, making consumer adoption much more likely.
Takeaway: Publish an RFC document before committing to widely impactful changes. This gets consumers invested in the decision and allows them to call out problems preemptively.
Build in backward compatibility
If you work on a dedicated systems team, it can be hard to remember that you are merely one of many dependencies to your consumers. Upgrading to the latest major design system version must be weighed alongside shipping exciting new features and business-critical bug fixes.
Mo dependencies, mo problems
With the rise of monorepo architectures (where many applications live together in a single repository and share a single set of dependencies), backward compatibility is more important than ever before. In this structure, upgrading a dependency version for one application could mean updating it for all applications in the repo, multiplying the number of migrations that must be handled at once.
“Backward compatible refers to a hardware or software system that can use the interface of an older version of the same product.”
Offering backward compatibility allows consumers to update to the latest and greatest version immediately, without requiring them to make changes to their applications. They can incrementally update their legacy designs and code as time allows, rather than having to wait for product management to set aside a large block of time, time that may not come for months, years, or ever.
Take the pain away (from your consumers)
There are cons to backward compatibility. It is, in essence, simply kicking the can down the road. The features in question will still be removed in a breaking manner someday. Backward compatibility also means added complexity and tech debt in your system, and can hold you back from improvements like increased performance. The advantage, however, is that this allows the design systems team to “take the pain” in order to maintain a good relationship with consumers and help ease them into an upgrade, rather than shock them with a difficult-breaking major release.
Using analytics and talking to your key consumers can help you determine when offering backward compatibility is worthwhile (hint: most of the time). Counting the places a deprecated feature is used can help you determine how long to maintain backward support.
Publicize deprecation schedules
Backward compatibility should come with a publicized deprecation schedule or a set amount of time (or number of versions) that the legacy feature will be kept around before finally being removed. Doing this keeps the design system team/s accountable and sets a clear timeline for removal in the future.
Takeaway: Backward compatibility is important for large applications. In the monorepo world where many applications may share a single dependency, it’s becoming even more important. It can add complexity to your system, but it’s worth the trade-off.
Support (and document) multiple major versions
Alongside backward compatibility for specific features, it’s also wise to support whole previous major versions of your design system for a set amount of time.
Version your documentation
Generally speaking, it’s not difficult to keep an old copy of your library’s code around for consumers to import, or to patch the odd previous-version vulnerability. The true cost of supporting previous majors lies in versioning and maintaining your designs and documentation.
Maintaining multiple versions of designs and documentation can be much more complicated and time-consuming than maintaining legacy code, as version management systems for these things are new and not widely adopted. Because of this, designs and docs are often updated to match the latest version, overwriting prior versions. My hot take: if you don’t provide docs or designs for previous versions, you aren’t actually supporting them.
Everything has an expiration date
You’ll inevitably need to support at least one major version back. But don’t support legacy versions forever — in most cases supporting one or two major versions behind the latest stable release is sufficient. Sometimes the looming threat of becoming unsupported is what a future team needs to get a migration prioritized. Like with legacy feature support, it’s beneficial to publish a deprecation schedule for previous majors and to give consumers plenty of warning before they become unsupported.
Takeaway: If you don’t provide docs or designs for previous versions of your design system, you don’t support those versions.
Like everything else in the systems world, successfully managing a breaking change is an exercise in striking the right balance. Before committing to a breaking change, do your research. Supplement analytics with in-depth case studies of key consumers. Use semantic versioning along with publicized release and deprecation schedules so your consumers know when to expect a change. Build confidence in that change by publishing an RFC proposal. When possible, include backward compatibility in new versions, and support a limited number of legacy versions alongside versioned designs and documentation.
I hope these tips help ensure your next breaking change is a successful one. If you want to share your own tips or chat about design systems or accessibility, reach out. I’d love to hear from you!