At the end of 2019, I was able to help the Design Systems team at Sprout Social propose a residency program, and I participated as the first resident. A residency program is a way for developers and designers to join the Design Systems team for a period of time before returning to their normal product teams. I was able to complete several valuable projects as well as learn more about our systems, collaborating as a product organization, and how residency programs can work within an organization. I talk more about the value of rotating new engineers to work on existing systems here. My projects focused on making contributions to the Design System’s component library, internally known as Racine, easier and less confusing and build tools to help educate the users of the library.
Contributors of icons to Racine might have noticed a Design Systems team member reviewing each one and suggesting changes to clean up and optimize the file. At Sprout, we commit our icon SVG files to the Racine repository. This gives us a single source of truth for the icons we use around our application. Often these icon SVGs are pulled from different places and are inconsistent or include unnecessary information for one reason or another. Having cleaner icon SVGs means having icons that are faster, more consistent, and overall easier to maintain. The Design Systems team manually reviewed incoming icon SVGs and suggested changes, which the contributor would then need to incorporate and then submit again. This was a slow and tedious process. Despite the Design Systems team’s commitment to the review process, this was simply not scalable as we continue to add and maintain more and more icons.
This is where automation and tooling can help us! By exploring what changes the Design System team was requesting, we could work to distill those steps into something we could run on every icon modification and addition. We utilized a tool called svgo which is a plugin-based tool for optimizing SVG files. This tool gave us the flexibility to enforce different custom rules for our SVG files. I was able to wrap a command-line interface (CLI) around svgo so that we could run it on staged files on commit and the entire codebase during Continuous Integration (CI).
We have a set of default and custom rules turned on that will strip out elements and attributes that are unnecessary and in general clean up and optimize our icons. This CLI tool is used on commit on staged files (think prettier for icons!) for that instant feedback and cleaner git log. It is also used as part of Racine’s pull request CI so that unchecked icons SVGs don’t slip through the cracks.
What’s great about building the linter in a flexible manner is that it opens the door to developing future custom optimizations and fine-tuning the process. The development of a tool or CI is never done; it is an ongoing process.
As the Design Systems team learns of friction points or possible improvements they can adjust the process so that the tooling serves the developers. You never want linters and validation CI to put up roadblocks and create work without a good reason and those reasons can change over time. The flexibility of this tool hopefully means that the framework can remain as the inevitable adjustments to the process happen over time.
In fact, since first writing this, the Design Systems team has adjusted the icon development process. Now the source of truth for icons is in Figma (a design tool) and owned by designers. The icons get exported from Figma, run through the icon linter, and merged into the Github repo for their use in the application. Due to the flexibility of the linter, adjusting the process was remarkably easy.
A major project for the Design Systems team for the past couple of years has been creating and maintaining a component library called Racine (often referred to as Racine 2 for reasons that will become clear) separate from the Sprout application code. Before that, web developers had been using a component library also called Racine (or Racine 1) that was embedded and entangled in the application code. Racine 1 had its own documentation site that was built with the web application. Now that Racine 2 is widely used and reached a parity with Racine 1 that we were comfortable with, we decided it was time to deprecate the use of Racine 1. This means deprecating the documentation site. Having two Racines and two documentation sites is confusing, especially for new hires. Plus, it was slowing down the application build times for something no longer widely used.
Deprecation is not the same thing as deleting. We did not want to completely delete the Racine 1 documentation because we knew there might be developers who still use it or might need it in the future. But we still wanted to dissuade its use and point developers to Racine 2 instead.
The default behavior of something can be a powerful tool for persuasion. I modified the default behavior of the Sprout application’s build process to deprecate the Racine 1 documentation. Instead of always building the documentation by default, I switched it to only build the documentation when you passed a flag. This way, developers would only build the documentation when they explicitly ask for it. This sped up the average build times for the application and still allowed the Racine 1 documentation to be accessible if needed. If a developer tries to navigate to the documentation page without building them, they will see a message pointing them to use the flag. This increases the visibility of the flag that is now necessary to access the documentation.
This all helps with build times and deprecates the use of Racine 1 documentation. But the code powering the documentation site still exists in the application repo. This is code that can confuse developers and add to repository bloat. We want to tidy up after ourselves when we know the documentation site isn’t used anymore. How do we know when we can fully remove that code? I added a trigger to the Racine documentation site that will log whenever someone visits the site. This gives us the observability and transparency into how many people are using the flag to build and view the Racine 1 documentation. If we find after several months that no one is using the Racine 1 documentation it will be relatively safe to go back and delete it from our codebase. This observability affords us the confidence to remove the deprecated code knowing that no one is using it.
Another part of deprecating Racine 1 and moving developers to Racine 2 is the deprecation of Racine 1 components. There are many components in Racine 1 that have equivalents in Racine 2 that are newer and currently being maintained. It is confusing to have two components that do the same thing and we want to make it obvious which one developers should use. Similar to the Racine 1 documentation deprecation project, we want to dissuade the continued use of the deprecated components without outright banning their use (there are used in many places, and forcing the immediate removal of them would take its toll on the product roadmap and most likely introduce bugs). We need a transitory state where we can prevent the new use of the deprecated components and track the existing use of them while still allowing developers to ship product features.
I was able to utilize some existing tooling in the Sprout application to achieve these goals. ESlint is a tool we use in the application to enforce rules about how we write code. There is a rule that allows you to mark specific exports (in this case components) as deprecated with a custom message. Then whenever a file imports that export, the developer will get a warning saying to use the equivalent component in Racine 2. This gives developers inline feedback and allows them to learn that a component that they are trying to use is deprecated and discover newer components while they are writing code. Instead of checking and maintaining a document susceptible to falling out of date, eslint rules alert you to deprecations while you write code. By colocating this information with when you would need it, the information is not forgotten and more likely kept up to date. ESlint alone will not track nor allow existing uses of deprecated components. For that, we can use a tool called Esplint. Esplint allows and tracks existing violations of an eslint rule while preventing new violations. Read more about it here.
Armed with the ESlint deprecation rule and Esplint, we can specify all the Racine 1 components with Racine 2 equivalents as deprecated. When a developer tries to import those components, they will be pointed to a better, newer alternative. Existing uses of those deprecated components will remain in the codebase with a warning so that developers can go back and switch them out in their own time. This dissuades the continued use of the deprecated components without outright banning their use. It also helps us track the current usage so that eventually we can remove the code for the deprecated components entirely. What is also great about these solutions is that it is not Racine specific. We can use these tools to deprecate anything in the application codebase. If we need to migrate the application to new number formatting utility functions, we can deprecate the old ones and point them to the new ones (we already do this with our deprecated number formatters!).
Deprecation is a powerful tool when it comes to migrating developers to a newer, better solution (in this case Racine). It helps educate developers by pointing them to the right solution without forcing them to block their product roadmaps. Both the documentation site and component deprecation highlight some powerful solutions when it comes to phasing out features. We have the concept of using defaults and inlined/colocated information to educate and persuade behavior away from the deprecated feature. Another concept is still allowing the use of the deprecated feature in cases when developers still need access (we can’t anticipate 100% of future use-cases so we need some continued accessibility). We don’t want to force them to change anything unless it’s critical or time-sensitive. And observability into usage so that we can eventually tidy up and remove the feature fully. These concepts will help you deprecate and eventually remove any feature smoothly.
I was able to participate as the first Design Systems resident for the month of December 2019 and half of January 2020. There were a lot of things that worked well and some things that could use work. I just want to call out a few things that worked well or could use some work. One of the most valuable meetings I had with the Design Systems team at the beginning of the residency was our brainstorming meeting. We met and brainstormed issues and possible solutions with the Design Systems currently. I came out of that meeting with a lot of great ideas and a better idea of what things I could be working on for the next month and a half (many of which I was able to complete over the course of the residency). I found that meeting to be a very valuable way of kicking off the residency since we arrived at projects that interested and inspired me.
One of the areas that could use improvement is the structure of the residency. Often I felt isolated from any team and working in limbo. I do think a lot of this had to do with the Holidays and PTO schedules not lining up or the lack of an end date but it is something to pay attention to. I know I am the only person who likes stand-ups, but some sort of formalized check-in process would help the resident make progress and avoid flailing in place. I think I was able to accomplish a lot on my own since I am more familiar with Design Systems and Racine and had a good idea of how I was going to complete the projects; I worry about a less experienced resident left to their own devices. Some sort of structure to the residency could go a long way to help in this area.
All in all, I think residency programs could be the right path for Sprout and other organizations. I still believe in a lot of the benefits outlined in the original proposal. I believe they can help with education, knowledge transfer, give developers opportunities to own larger projects, and staffing issues. I think exposure to other teams and projects even temporarily can help a lot with cross-organization communication and system simplification.
- Deprecation is a powerful tool when switching to a newer system. It clearly communicates the desired developer behavior without forcing it too early.
- Changing the default behavior of a system can be enough to push users/developers toward your desired outcome.
- Colocation of documentation will make sure that users/developers have the information when they actually need it and will also help keep eyes on the documentation and keep it up-to-date.
- Make sure you have observability into the usage of deprecated systems so you know when you can delete things! Deprecation is not the end goal, it’s a means to delete something. So make sure you know when you can switch from deprecation to deletion.
- Validation CI/Linters should be flexible so it’s not a roadblock. Adjustments to process are inevitable so make sure your CI has the ability to adapt. If a CI is too brittle, you’ll find yourself rewriting it before you know it.