← All writing
#FrontendSystemsJun 16, 2026 · 6 min read

One Component Library, Three Applications

What changes when three apps depend on the same components — and why the components are the easy part.

A while ago I noticed the same button living in three different applications. Not literally the same — three buttons that were supposed to be the same. One had the correct padding. One still used last year's hover colour. The third had quietly grown a second variant nobody could remember adding. Each had been written by a different person, at a different time, to solve the same small problem.

That is a moment most teams eventually reach: you realize you are maintaining one idea in several places, and the copies have started to disagree with each other. The reflex is to go fix the three buttons. The more useful reaction is to ask why there were three buttons at all.

So we did the obvious thing and pulled the shared pieces — buttons, inputs, modals, the layout primitives — into a single library that all three applications would depend on. What surprised me was that the components were never the difficult part. Releases were. Upgrades were. Deciding when something belonged in the library and when it didn't was harder than building the component itself.

The pattern underneath

The shape repeats across many products. You have several applications — a customer-facing app, an internal tool, a second product that shares the same look — and they all need the same building blocks. Rebuilding those blocks per app produces drift: the same element behaves slightly differently in each place, and every visual or behavioural change has to be made several times, by hand, hopefully consistently.

A shared library inverts that. There is now one definition of a button, and three consumers of it. Change the definition once and, in principle, all three applications move together. The promise is appealing and basically correct: you trade three cheap copies for one well-maintained source of truth.

The catch is that you have also created a new kind of object — code with consumers — and that quietly changes what "making a change" means.

A shared library is not shared code. It is a product with consumers.

Why teams get stuck

In a single application, changing a component is a local act. You edit it, check the few places it's used, and ship. In a shared library, the same edit is a public event. Three applications will receive it, possibly at different times, possibly when their teams are in the middle of something else. The component is no longer yours to change freely; it is an interface other people build on.

This is where shared libraries tend to fail, and it usually isn't a technical failure. It's a coordination one.

Breaking changesripple to every appDuplicate runtimetwo Reacts, broken hooksNo ownershipbecomes a dumping groundShared libraries fail on coordination, not on components.The hard part is coordination between consumers, not the components themselves.

The first failure is the breaking change that ripples outward. Someone renames a prop or tightens a default, ships it, and three applications break — or worse, two break and one silently renders wrong. After this happens a couple of times, teams get scared of the library. They stop upgrading, each app pins an old version, and now you have three copies again, just with extra steps.

The second is a duplicated runtime. For a React library, shipping React as a normal dependency means each consumer can end up with its own copy, and hooks start throwing errors that make no sense until you find the two Reacts. The fix is well known — peer dependencies — but the lesson behind it is the real point: a library and its consumers have to agree on a shared environment, and that agreement has to be explicit.

The quietest failure is ownership. A shared library sits between teams, so it easily belongs to no one. Nobody owns its releases, its changelog, or the job of saying no to a one-off request — so it slowly fills with special cases until it's just three apps' worth of code in a trench coat.

A better mental model

The shift that makes shared libraries work is to stop treating the library as a folder of shared code and start treating it as a small product with users.

A product has a contract with its users, and for a library that contract is versioning. Semantic versioning is not bureaucracy here; it's how you tell three teams what a change will do to them. A patch is safe. A minor adds something without taking anything away. A major might hurt, so it comes with notice. Once versions mean something, consumers can upgrade on their own schedule instead of being yanked along with every commit.

A product also has boundaries it defends. Peer dependencies are one — the library declares "I expect you to bring React, and we must share one copy." The component API is another. The more you can make changes additive — a new variant rather than a changed default, a new prop rather than a renamed one — the less often a change becomes a public event that breaks someone. Removing things is the expensive operation, so you deprecate first and remove later, on purpose.

And a product has an owner — not necessarily one person full-time, but someone accountable for the changelog, the release, and the unglamorous job of declining the fourth slightly-different button. Much of the value of a shared library comes from the requests it refuses, because each accepted exception is a small tax every consumer pays forever.

What I learned

If I take one thing from three applications sharing one library, it's that the components are the easy part. The discipline is in treating the shared code as an interface: version it so consumers know what each change costs them, keep changes additive so upgrades stay boring, share the runtime explicitly so the environment can't fragment, and give it an owner so it doesn't silently absorb every exception.

Before: a shared library is shared code.

After: a shared library is a product, and its real job is to make the next change cheap across every app at once.

Do that, and the library delivers what you hoped: one change, three applications, no drift. Skip it, and you end up back where you started — three buttons that were supposed to be the same — only now they're harder to fix, because they're hiding in a dependency.