Sep 04, 2025·8 min read

Shared libraries: when to extract code and when to copy

Shared libraries can save time or slow teams down. Learn when to extract code, when to copy it, and who should own upgrades.

Shared libraries: when to extract code and when to copy

Why shared code turns into team friction

Code can look shared before the teams around it actually work the same way. One product team wants to ship on Tuesday. Another wants to wait for a larger release. A third cares most about stability. The code sits in the middle, so a small change becomes a conversation instead of a commit.

That is when shared libraries start to hurt. A button tweak, a logging fix, or one new config flag sounds small. In practice, someone asks who else uses it, who will test it, whether the version bump is safe, and whether the change should wait for the next release. Ten minutes of coding turns into a week of messages.

Most of the pain comes from timing. Product work follows customer deadlines, bug reports, and sales commitments. Shared code follows compatibility rules, version bumps, and upgrade windows. Those clocks rarely match. When they drift apart, one team waits or ships a workaround.

Control matters just as much. Teams move faster when they can open a file, fix it, and deploy. They slow down when the code belongs to another group with different priorities. After a few rounds of waiting on someone else's backlog, people stop touching the shared code unless they have no choice.

The pattern is easy to recognize. Teams copy older versions into their own repos and patch them locally. Nobody wants to upgrade because they fear breaking another team's work. Small differences turn into debates about whose use case should shape the design. Before long, people build around the library instead of through it.

At that point, the code is no longer shared in any useful sense. It is just a dependency teams tolerate. This happens in startups and large companies alike. The coding problem is often small. The ownership problem is what makes it expensive.

What should stay inside one product

Some code looks reusable the moment a second team notices it. That is often too early. If the code still changes to fit one product's workflow, keep it inside that product.

UI code is where teams make this mistake most often. A checkout form, admin table, or onboarding flow may look similar across products, but the small differences add up fast. One team wants three fields, another wants five, and soon every design tweak turns into a negotiation.

Rules that change every sprint should stay local too. Pricing logic, approval steps, trial limits, and edge cases shift as teams learn what users need. If you move those rules into shared libraries too soon, simple product changes start depending on release timing, version bumps, and other teams' priorities.

Code should also stay local when only one team really understands it. That is not a criticism. Sometimes one group built a feature under deadline, knows the weird cases, and can change it safely in an hour. Moving that code into a shared package before other teams can maintain it just spreads confusion.

Early experiments are another poor fit for reuse. If a team may delete the feature next month, do not polish it into a library today. Copying 100 lines of code is often cheaper than creating a package, writing docs, adding tests, and supporting upgrades for a feature that may disappear.

Copying can be the cleaner choice

Copying code sounds messy, but sometimes it is the honest option. Two teams can start with the same base, move in different directions, and learn what actually stays stable.

That is common in startups. One team may test a sales workflow while another builds a self-serve version. The screens may start close, yet after a few weeks the differences matter more than the shared starting point.

A simple rule helps: keep code inside one product when its shape depends on one team's pace, language, and daily decisions. Extract it later, after the rough edges stop moving. You will ship faster and avoid turning small edits into cross-team process.

What belongs in a shared library

Code earns the right to be shared when more than one team depends on the exact same result. The best candidate is not code that merely looks similar. It is code that must behave the same way in every app.

A repeated bug is one of the clearest signals. If several products keep fixing the same date parsing issue, permission check, or API signing logic, the problem is no longer local. Separate copies mean each team will miss the fix at a different time.

Stable problems fit well in shared libraries too. If the rules change every sprint, a library creates more meetings than value. If the job is clear and unlikely to move much, such as validating a file format, applying the same security policy, or talking to one internal service, shared code can save real effort.

The real test is simple: do teams need the same behavior, or do they only have similar code today? Similar code often drifts for good reasons. One product may need looser validation, different error messages, or a different retry policy. If those differences matter, keep the code inside each product.

Ownership matters as much as the code itself. A library needs one team or one person who can test it, cut releases, review changes, and answer a plain question: "Can we change this without breaking everyone?" Without that owner, shared libraries turn into a pile of half-kept promises.

A good library usually does one narrow job, fixes the same issue in several apps, needs consistent behavior everywhere, and has a clear owner. When those conditions are true, sharing the code tends to reduce work instead of spreading it around.

How to decide in five steps

A short checklist beats a long debate. Most bad shared libraries start when teams extract code too early, before they agree on what is actually the same and who will carry the maintenance work.

  1. Start with where the code lives now. Write down the repo, package, service, tests, and product rules around it. If the code already depends on product-only models, UI text, or billing logic, treat that as a warning sign.
  2. Write the exact behavior each team needs. Be picky. "Almost the same" usually hides real differences in edge cases, error messages, timing, or permissions.
  3. Separate stable parts from moving parts. Date parsing or a narrow API client may stay calm for months. Pricing rules, onboarding steps, and sales workflows often do not.
  4. Pick one owner before you extract anything. One team, or even one person, should approve changes, review pull requests, and decide when a request belongs in the library or should stay local.
  5. Set release and upgrade rules in plain language. Decide how you will publish versions, what counts as a breaking change, who upgrades dependents, and how long older versions can stay in use.

A small example makes the choice clearer. If two products both send email, but one needs basic templates and the other needs regional legal text, custom retry rules, and audit logs, copy the code for now. If both use the same provider, the same failure handling, and the same tests, extracting that narrow part makes sense.

If you get stuck on step 2 or step 4, do not share the code yet. Copy it, keep moving, and review it again after a few weeks of real use.

Who owns the library day to day

Cut Upgrade Friction
Plan versioning and support windows that fit product deadlines instead of blocking them.

A shared library needs one owner team. Not a working group, not a rotating list, and not "everyone who uses it." When ownership is fuzzy, small fixes sit in chat threads until someone gets annoyed enough to patch around the problem.

Pick the team that knows the code best and feels the pain when it breaks. That team makes the final call on design, release timing, and what belongs in the library at all. Other teams should have input, but they should not need to negotiate every line.

In practice, the owner team reviews and approves changes, cuts releases, answers usage questions, and plans maintenance work in its own backlog. That last point matters more than people think. Support work is real work. If bug fixes, upgrade requests, and compatibility checks live outside the owner team's backlog, nobody will budget time for them.

Keep the request path simple. Users should know exactly where to go when they need a change. A single issue queue, a clear template, and one named contact are usually enough. Avoid private messages and side deals. They feel faster for one team, but they create confusion for everyone else.

Approval should stay with the owner team even when another team writes the code. That rule protects the library from one-off product needs. It also keeps releases steady, because one team sees the full picture: old consumers, upgrade costs, and the long-term shape of the API.

In a small company, the setup can stay simple. One product team owns the library, and the CTO or fractional CTO steps in only when priorities clash. Day to day, the owner team keeps it moving.

How to handle upgrades without blocking work

Product teams ship on their own deadlines. Library teams ship on a different clock. If those schedules collide, even a small upgrade turns into a meeting, a delay, or a rushed patch.

With shared libraries, boring release rules beat clever ones. Clear version numbers do most of the work. Patch means a fix. Minor means a safe addition. Major means something will break and teams need to plan for it. People should know the risk from the version number alone.

Random breaking changes create most of the friction. Group them into planned major releases a few times a year instead of slipping them into normal updates. That gives product teams time to book the work, run tests, and avoid changing code during launch week.

Older versions should stay supported for a fixed window. Six to twelve months works well for many internal libraries. During that window, the library owner fixes serious bugs in the previous major version while new features go into the current one. Teams can upgrade when they have room, not when another team gets impatient.

The release routine does not need much ceremony. Use version numbers that match the actual risk. Publish one change note for every release. Put breaking changes at the top, show the upgrade steps in a few lines, and say when support for the old version ends.

That note does not need polish. A few plain sentences are enough: what changed, who needs to care, and what to update. If a team can read it in one minute, they are more likely to act on it.

Busy releases also need an escape hatch. If a product team is onboarding a large customer or shipping a major feature, let them skip the upgrade and catch up later. A delayed upgrade is usually cheaper than forcing a change into an already tense release.

A simple example with two product teams

Bring Order to Releases
Set plain release notes, breaking change rules, and upgrade paths your engineers will actually use.

Team A builds a signup form for a SaaS product. Team B builds a customer intake form for an internal tool. Both forms ask for the same basics: name, email, phone number, and company details. At first, each team writes its own validation because copying a few checks is faster than setting up a shared library.

That choice is fine early on. The forms look similar, but they do different jobs. Team A wants short, friendly errors for new users. Team B needs stricter rules and extra notes for staff. Even the field order is different because each product has its own flow.

The trouble starts after the same bug appears several times. One team fixes email validation so it catches spaces at the end. The other team forgets. Later, phone numbers with country codes fail in one product but pass in the other. Now both teams waste time comparing small fixes instead of moving on.

They should not extract the whole form system. That would force both teams to argue about layout, labels, and every little UX choice. They should extract only the part that truly matches across products: the raw validation checks.

The shared code stays small. It trims spaces before validation, checks email format, normalizes phone numbers, rejects empty required fields, and returns simple error codes. Each team still controls the product-facing layer. Team A writes error text like "Please enter a work email." Team B writes "Email is required for account creation." Team A keeps a one-column mobile layout. Team B keeps a denser form that fits internal workflows.

This split works because ownership stays clear. The shared package owns the logic that should behave the same everywhere. Each product owns how the form looks and how errors appear to users.

That is usually the safe line with shared libraries. Share the boring rules that should never drift. Keep anything tied to product behavior, wording, or screen layout inside each codebase.

Mistakes that make reuse harder

A lot of reuse fails before the library does anything useful. Teams often extract code as soon as they spot two similar files, but one case is real and the other is still a guess. That creates a package shaped by assumptions, not by steady use.

Similar code is not always the same code. Two products might both send invoices, but one may need approvals, tax rules, or custom billing cycles. If you force both into one shared library too early, every change turns into a fight over options, flags, and exceptions.

Another common mistake is stuffing unrelated work into one package. A "common" library starts with one helper, then grows to include auth checks, UI pieces, logging, and business rules. It feels tidy for a month. Later, nobody knows what can change safely, and a small update carries risk for teams that never asked for half the package.

Ownership causes just as many problems as design. When every team gets a vote on every change, progress slows to a crawl. A small fix should not need three meetings and broad agreement. One team should own the library day to day, review changes, and decide the default direction.

Upgrades also break trust fast. If teams must move to the newest version right away, they will avoid the library, pin old versions, or copy the code back into their product. Active projects need room to finish current work before taking on upgrade risk.

Watch for a few early warning signs:

  • code was extracted before there were two proven use cases
  • one package mixes several unrelated jobs
  • every change needs approval from too many people
  • release pressure forces upgrades at the worst time
  • the code looks similar, but each product still needs different fixes

Good shared libraries stay narrow, boring, and owned by someone specific. If a package needs constant negotiation, it is probably too broad or too early. In many cases, copying 50 lines twice is cheaper than sharing 500 lines badly.

A quick check before you share code

Review Your Architecture
Get a CTO level view on package boundaries before more teams depend on the wrong code.

Pulling code into a library feels tidy, but tidy is not the same as useful. A shared package helps only when it removes repeat work without adding meetings, release delays, and upgrade drama.

A fast test works better than a long design doc. If the answer to any of these questions is shaky, keep the code inside one product for now:

  • Are two or more teams solving the same problem in almost the same way, rather than using similar code by coincidence?
  • Can one team own releases, bug fixes, and support for at least the next six months?
  • If another team skips one version, can they still ship features without getting stuck?
  • Can someone describe the library in one plain sentence without a long caveat?

That last point matters a lot. If the description turns into "it handles auth, logging, config, some caching, and a few helper utilities," it is probably not one library. It is a pile of unrelated code that will confuse every new team that touches it.

Ownership is the other common failure point. Shared libraries die when everybody can change them and nobody has to maintain them. One team should decide releases, review changes, and say no when a request helps one product but makes the package harder for everyone else.

Upgrade freedom is easy to miss. A library should not force every team to update the moment one team needs a fix. If skipping a release breaks normal work, the dependency is too tight.

A small example helps here too. If two products both need the same date formatting helper, copying it may be cheaper. If they both need the same billing rules, tax logic, and audit behavior, a shared library makes more sense because the business rule is actually the same.

Good shared libraries have a narrow job, a clear owner, and a release path that does not slow everyone down.

What to do next

Start with a rule that feels almost too simple: copy first, extract later. Put it in writing. When two teams think they need the same code, let each team ship with its own copy until the pattern stays stable for a while. Similar code is not enough on its own. Reuse starts to make sense when the same rules, edge cases, and release pace keep showing up across products.

Then review your current shared libraries one by one. Most teams already have a few packages that create more discussion than speed. Put one owner on each library. That person does not need to write every change, but they should decide scope, approve breaking changes, and keep versioning sane.

A short cleanup pass usually helps more than another architecture debate. Mark every library as owned, unowned, or ready to retire. Remove packages that teams avoid, fork, or complain about every sprint. Split stable utility code from product-specific logic. Write one clear upgrade rule that teams can follow without asking permission each time.

If no team wants to own a library, stop pretending it is shared infrastructure. Retire it. A copied module inside one product is often cheaper than a shared dependency that nobody trusts and nobody updates.

This also needs a date, not just a good intention. Pick a day in the next two weeks. Spend 60 minutes with engineering leads. List every shared package, name an owner, and decide which ones stay, which ones get split, and which ones go away. That single meeting can remove months of low-grade friction.

If you want an outside review, Oleg Sotnikov at oleg.is does this kind of Fractional CTO work. His approach is practical: decide who owns what, keep product-specific code local, and share code only when it removes repeat work instead of creating more coordination.

Frequently Asked Questions

When should we copy code instead of extracting a library?

Copy the code when one product still shapes it. If the logic changes with one team's workflow, release pace, or edge cases, keep it local for now.

A small copy often costs less than a shared package that creates review delays, version work, and cross-team arguments.

What is the clearest sign that code belongs in a shared library?

Share code when two or more teams need the same behavior, not just similar files. Repeated fixes for the same bug, the same validation rules, or the same service client usually point to a real shared need.

If each team still wants different rules or different failure handling, leave it inside each product.

How many use cases do we need before we share code?

Wait until you have at least two proven use cases that stayed close over real work. One current use case and one guessed future use case usually leads to a library that tries to solve too much.

Give the code a little time. If both teams keep making the same changes, extraction starts to make sense.

What code should usually stay inside one product?

Keep UI flows, product wording, pricing rules, approval steps, and fast-changing business logic inside the product. Those parts drift quickly because each team learns from different users and deadlines.

Local code lets a team change and ship without asking another group for permission.

Who should own a shared library?

Pick one owner team, not a committee. That team should review changes, cut releases, answer usage questions, and decide what belongs in the library.

Choose the team that knows the code best and feels the pain when it breaks. That keeps decisions fast and support work visible.

How small should a shared library be?

Keep it narrow. A good library does one job that stays stable across products, like validation rules, API signing, or a small service client.

Once a package mixes UI, logging, auth, and business rules, teams stop trusting changes because every release touches too much.

How do we handle upgrades without blocking product work?

Use plain version rules and stick to them. Patch should mean a fix, minor should mean a safe addition, and major should mean planned breaking work.

Support older major versions for a fixed window so product teams can upgrade when they have room. That removes a lot of release tension.

Should we put UI components into a shared library?

Most teams should avoid sharing whole UI components too early. Screens that look alike often need different fields, layouts, copy, and validation details after a few weeks.

Share the boring logic under the UI first, such as formatting or raw validation, and let each product own the screen itself.

What if teams need the same logic but different user experience?

Split the problem at the right line. Put the same low-level logic into shared code, then keep product-facing text, layout, and workflow inside each app.

That gives teams consistent rules where they need them without forcing the same user experience everywhere.

What should we do with a library nobody wants to maintain?

Treat that as a warning sign. If no team wants to own the package, retire it or move the code back into the products that still use it.

Shared code without ownership turns into delay, local patches, and stale versions. A copied module often works better than a dependency nobody trusts.