Failing Forward at Design Systems
When you set out to build a design system, your head is full of components, tokens, and documentation. That's what a design system is, right?
It's tangible work. You can shape it exactly how you want, and it feels like real progress. Then you meet the reality of how your colleagues actually work, and you find out the hard part was never the building.
We learned that the hard way. My team spent four years building a design system for a product with over 20 years of history and all the legacy that comes with it. Most of our assumptions turned out to be wrong, but each time one broke, it pushed us somewhere better than where we'd planned.
The territory we inherited
The product was capable. Really capable. But you wouldn't know it by looking at it. The UI felt dated. If you judged the product by its interface, as people usually do, you would severely underestimate what it could do.
This wasn't for lack of effort. Teams were working hard, but they spent most of their energy extinguishing fires caused by decisions made years ago.
Technical debt had compounded to the point where even small UI changes felt risky. "We can't build that" became a reflex, not because the developers lacked skill, but because the codebase had made ambition expensive.
So the plan became to split the monolithic software into multiple standalone products. The hope was that this would bring technical and organizational benefits. Smaller codebases, independent teams, modern stacks. If the products were going to live apart, they'd need something to keep them speaking the same language. A design system seemed like the natural fit: shared components and a shared language across products.
Building the map
We put together a small team, bootstrapped a component library with a modern tech stack, and paired it with an updated visual design that had been approved long ago but never made it into production.
Around the same time, one of the product teams was about to rebuild their product from the ground up. We jumped on the opportunity. A greenfield environment. The perfect test case.
And it actually worked. The pilot team delivered something we were proud of. Components that used to take days to build from scratch were available out of the box. We ran into growing pains, but that was expected. The proof was there.
Crickets
We now had a real product to point at. Unfortunately, nobody cared. The rest of the company was still fully invested in the legacy system, and the plan to split the monolith never gained traction. We had built for a future the organization wasn't moving toward.
When we showed up with our shiny new tools, our colleagues looked at their workload and said "I don't have time to learn this." They weren't being difficult. They were being rational. Learning something new feels like too much of a gamble when you're already struggling to keep up.
Meeting them where they were
The approach wasn't working. Our assumption had been that teams were just waiting for us to hand them something better. They weren't. We met with them to understand their perspective.
The answer might seem counterintuitive: we disguised our new design system to look exactly like the legacy app. We matched every visual detail so that anything built with the new components was indistinguishable from the old code. Under the hood, developers were working with modern tools. On the surface, nothing changed.
Pulling this off was possible because we'd made a bet early on and kept the visual layer of our components separate so they could adapt to different brands across the company. We didn't expect that to matter so soon. The old codebase had no tokens, no shared variables, just scattered CSS files that styled raw input and table elements directly. So we rebuilt the whole look from scratch with our own tokens and ended up normalizing a lot of the visual inconsistencies along the way.
Then we focused on making the transition painless. We wrote guides. We gave training sessions. We improved the dev environment.
But what actually mattered was showing up. We sat with colleagues during their first steps with the design system. We helped with the first app, stayed for the first obstacle, debugged alongside them. We weren't a library they could install. We were a team they could call.
The trap
Slowly, it started working. The narrative shifted from "we can't build that" to "we can build that... most likely." One team after another started using the design system without us having to ask. In product demos, more and more features were built with our tools. The numbers looked great.
So yay! We did it! ...right?
Not quite. Adoption was up, but teams were still building inconsistent interfaces. Same buttons, same inputs, same building blocks. The pages looked consistent at first glance, but the way they worked and felt told a different story.
Our colleagues knew how to use the components. They were productive. But knowing how to use a component is not the same as knowing when to use it or how to combine them. The system had little opinion about how things should come together. Teams approached the same user flows differently and arrived at results that were close enough to look right, but different enough for users to notice.
The moment it became obvious was a quarterly product demo. Several teams had shipped new features with our components, and instead of looking cohesive, the results were all over the place. A parking policy configurator had become needlessly complex. An access management flow had been crammed into a dialog when it needed a proper page, making the experience frustrating to use. Teams had reached for every component they could just because it was available. The implicit rules of the legacy system, the ones nobody had written down but everyone followed, were suddenly gone. Components gave teams vocabulary, but nobody had agreed on the grammar.
So we pivoted again.
The patterns we ended up writing didn't come from an audit or a workshop. They came from conversations. When we asked teams what support they were missing, nobody said "we need documented patterns." But what they described pointed us there: teams were solving the same problems differently and didn't always know which approach to pick. So we started writing down what we'd already been figuring out together in pairing sessions. Less "here's a button," more "here's how to load data and keep people in the loop."
Failing forward
For most of the journey, it felt like we kept getting it wrong. But looking back, the pattern is clear: we kept confusing the map for the territory.
Our documentation, our component library, our adoption metrics: that was the map. Organized, clean and controlled. The territory is what we kept crashing into. Layers of workarounds in a codebase that never stopped growing. Tight deadlines forcing teams to take the same shortcuts they'd been taking for a decade.
Every time we treated the map as truth, the territory corrected us. Being ignored pushed us to invest in developer experience and hands-on support. Inconsistent interfaces showed us how much teams needed clear recommendations.
Each obstacle forced us closer to the people using our system, and that is where things actually got better.
We are still failing forward. The territory keeps changing. But we've stopped treating the obstacles as setbacks. Because what got us here wasn't sticking to a plan. It was the willingness to throw it out.