← All writings ES

The Hidden Cost of Shared Components in Multi-Product Banking

Building a shared React component library for financial products sounds straightforward. After two years delivering it across four products at Scotiabank, here's what actually happens.

We launched the shared component library with good intentions: one Button, one Input, one Modal — used consistently across every financial product. Two years later we were maintaining four divergent forks, a semver changelog nobody trusted, and a breaking change process that blocked releases across teams for days.

Nobody tells you this about shared component libraries in large organizations: the engineering problem is easy. The organizational problem is brutal.

The promise versus the reality

The promise is obvious. Shared components mean consistent UX across products, reduced duplication, one place to fix bugs. The reality after scale:

  • Product teams have different release cadences
  • One team’s “improvement” is another’s breaking change
  • Design decisions made for one product don’t generalize
  • Every consumer becomes a de facto stakeholder in every PR

At Scotiabank CCAU, we built the component library to serve the OneBank initiative — a platform where customers could apply for loans, credit cards, savings accounts, and insurance through a unified digital onboarding experience. Each product had its own team, its own deployment pipeline, and its own timeline. The component library was supposed to be the connective tissue.

The version drift problem

Within months:

ProductLibrary version
Loans3.4.1
Credit Cards2.8.0
Accounts4.1.0
Insurance2.7.3

Four products, four versions, none of them current. The Insurance team was two major versions behind because a v3 change broke their custom form orchestration and they hadn’t had capacity to migrate. The Credit Cards team had cherry-picked a fix from v3.2 by copying the source directly into their codebase — which meant they no longer benefited from future fixes to that component.

This is normal. Every large shared library I’ve worked with ends up here unless you actively fight it.

What actually causes drift

Version drift isn’t laziness. It’s rational behavior under constraint.

When a team is three sprints from a regulatory deadline, upgrading a major library version is not an acceptable risk. The calculus is simple: the cost of a failed upgrade (blocked release, regression discovered in UAT, emergency rollback) far exceeds the benefit of being on the latest version of a design system.

The real cause is that library upgrades are treated as maintenance work, not product work. They don’t appear in roadmaps. They don’t get sprint capacity. They accumulate until the gap is too large to close without a dedicated migration effort — which then requires its own planning, justification, and resourcing.

The structural fix

After two years of iteration, three things actually helped.

1. Separate layers of abstraction

We split the library into three packages:

@onebank/primitives    — unstyled base components (Button, Input, Modal)
@onebank/components    — styled, opinionated implementations
@onebank/forms         — form orchestration with validation

@onebank/primitives follows strict semver with a no-breaking-changes policy on the public API. Anything that would be breaking goes through a deprecation cycle of at least two releases. @onebank/components can evolve faster because it’s opinionated — teams that need to deviate from the standard behavior use primitives directly.

This immediately reduced upgrade friction. Upgrading primitives became low-risk (stable API, no style changes). Teams that cared about style consistency used components. Teams with custom design requirements used primitives and owned their own layer.

2. Automated codemods for breaking changes

When we shipped a breaking change, we shipped a codemod alongside it:

npx @onebank/codemods migrate --from 3 --to 4

The codemod handled ~80% of the mechanical migration work. It couldn’t handle every case — component behavior changes that required manual review still needed human attention — but it removed the fear factor from major upgrades. A migration that previously took a team two days of careful find-and-replace became a thirty-minute automated run plus focused review.

3. Greenkeeping as a team norm, not a task

The hardest change was cultural. We established a norm: every team runs Renovate with auto-merge enabled for minor and patch upgrades. Major upgrades get a one-sprint window after release.

This required trust in the library team — specifically, trust that patch meant “no behavior change” and minor meant “backward compatible.” We built that trust by being conservative. If we weren’t sure whether something was a breaking change, we called it a breaking change.

What I’d do differently

If I were starting over, I’d resist the single-library model entirely. Instead:

  • One primitives package with a stable, minimal API. No styles. Think Radix UI or Headless UI.
  • Per-product component layers that own their visual implementation.
  • A design token package that synchronizes visual constants (colors, spacing, typography) without coupling component APIs.

The insight is that consistency doesn’t require shared implementation — it requires shared contracts. A design token system gives you visual consistency across products without creating the organizational coupling that shared component libraries create.

The metric that matters

One thing I started tracking late but wish I’d tracked from day one: time to render a new page for a net-new engineer.

Not “time to set up the dev environment.” Specifically: how long does it take for a developer who is new to the product team (but familiar with the component library) to build a working, compliant, on-brand form page?

This metric captures everything. If the component library is well-designed, composable, and documented, a new developer can be productive in a day. If it’s not, they spend their first week reading source code to understand what validation="strict" actually does.

At our best, we got this to four hours. That’s the version of the library I’m proud of.