Shared Libraries in Microservices: The Real Cost
Shared libraries in microservices promise reuse but quietly recouple services through a versioned dependency. When they help, when they hurt, and the rules.
Part of Microservice Service Design: Boundaries That Hold
Shared libraries in microservices are where good intentions quietly rebuild a monolith. Pulling common code into a library feels like obvious reuse, but a shared library is a shared dependency, and a shared dependency is coupling. The moment that library contains anything that changes often, a single library update forces every consuming service to follow, and you have recreated the lockstep deploys microservices were supposed to eliminate.
The nuance is that shared libraries are not all bad. The right ones, stable and generic, save real effort. The wrong ones, holding business logic, are a coupling time bomb. Knowing which is which is the whole skill.
Why shared libraries are a coupling decision
Microservices buy independence at the cost of some duplication. That trade is the entire point: two services each having their own copy of some logic is often better than two services chained to a shared one, because independence is what lets teams move without coordinating.
A shared library inverts that trade. It removes the duplication and reintroduces the coupling, because now both services depend on the same artifact and must agree on its version. Whether that is a good deal depends entirely on what is in the library and how often it changes. This post is part of the Service design series.
Why are shared libraries risky in microservices?
Because the library becomes a hidden dependency that links services which are supposed to be independent. When the library changes, every service that compiles it in must eventually move to the new version, so one library change creates a wave of forced updates across teams. That is precisely the lockstep-deploy coupling that service boundaries exist to prevent.
The risk compounds with the number of consumers. A library used by 3 services is a manageable coordination; the same library used by 30 services means any change is a 30-team migration. The coupling scales with adoption, which is the opposite of what you want from “reuse.”
What should be in a shared library versus a service?
Put stable, generic, business-agnostic concerns in a shared library, and keep business logic and fast-changing code behind a service API. The dividing line is change frequency and domain-specificity: things that rarely change and mean the same to everyone are safe to share as code; things that change often or encode your domain should be called, not compiled in.
| Good in a shared library | Belongs behind a service API |
|---|---|
| Serialization / Protobuf generated clients | Business rules and workflows |
| Logging and telemetry conventions | Pricing, eligibility, domain logic |
| Auth token parsing (stable format) | Anything specific to one bounded context |
| Common protocol/transport setup | Anything that changes most sprints |
| Stable utility helpers | Data ownership and persistence |
The test is simple: if changing this would force many services to redeploy in lockstep, it should not be shared code. If it is stable, generic infrastructure that almost never changes, a library is fine and saves real duplication.
Prefer sharing contracts over sharing code
The most robust form of reuse in microservices is not a shared library at all; it is a shared contract. Instead of two services compiling in the same logic, one service owns the capability and exposes it through an API the other calls. The Protobuf schema or the service’s API becomes the shared thing, and it is shared at runtime, not at build time.
This matters because a contract decouples deploy cycles. When logic lives behind an API, the owning service can change its implementation freely as long as the contract holds, and consumers never recompile. When the same logic lives in a shared library, every implementation change is a library version that consumers must adopt. Sharing the contract keeps the independence; sharing the code spends it.
There is a performance tradeoff to acknowledge honestly: a shared library runs in-process, while a shared service is a network call with latency and a new failure mode. That cost is real, and it is exactly why stable, generic concerns (serialization, logging) are better as libraries, where the in-process speed is worth the small coupling. For business logic that changes often, the network cost of a service is the price of independence, and it is usually worth paying. Match the mechanism to how often the thing changes, not to a blanket rule.
How do you version a shared library across many services?
Use semantic versioning, hold compatibility within a major version, and let each service upgrade on its own schedule. The failure mode to avoid is a single canonical version that every service must move to simultaneously, because that turns a library bump into a fleet-wide synchronized deploy, which is the coupling you were trying to avoid.
The discipline that keeps a shared library healthy:
- Semantic versioning, so consumers know what a version change means.
- No breaking changes within a major, so upgrades are safe by default.
- Independent upgrade cadence, so services move when they are ready, not on a forced date.
- A deprecation path, so old majors can be retired gradually rather than in a big-bang migration.
- Narrow scope, because the smaller and more stable the library, the rarely it forces an upgrade at all.
If you find yourself needing to force every service onto a new library version at once, that is the signal the library is holding something that should have been a service.
Is a little duplication better than a shared library?
Often, yes. In microservices, a small amount of duplicated code across two services is frequently cheaper than the coupling a shared library creates, because duplication is a local cost while coupling is a global one. Two copies that evolve independently let each team move at its own pace; one shared copy chains both teams to the same version and release cadence.
This reverses the instinct most engineers bring from monolith work, where “don’t repeat yourself” is close to sacred. In a single codebase, DRY is almost always right because there is one deploy and one team. Across service boundaries, the calculus changes: the thing you are deduplicating now spans independently-deployable units, and removing the duplication reintroduces exactly the coupling the boundary was meant to sever.
The judgment call is what kind of code it is. Duplicating a stable, trivial helper is pure waste, so share it. Duplicating logic that two services happen to need today but will likely evolve differently is often wiser to leave duplicated, because forcing them to share guarantees a future where one team’s change breaks the other. Ask whether the two copies are likely to stay identical forever; if not, the duplication is buying you independence, not costing you discipline.
A shared-library checklist
Before you extract code into a shared library:
- The code is stable and generic, not business logic and not fast-changing.
- Changing it would not force many services to redeploy in lockstep.
- You considered exposing it as a service API (shared contract) instead of a library.
- It is semantically versioned with no breaking changes within a major.
- Consumers can upgrade on independent schedules.
- The library scope is narrow; it does not become a grab-bag “common” dependency.
- You are comfortable that the coupling it creates is worth the duplication it removes.
What I’d do differently
The trap I have watched teams fall into is the “common” or “shared” library that starts as a few utilities and grows into a dumping ground that every service depends on. By the time it holds business logic, it is the single most-coupled artifact in the system, and changing it is terrifying because it touches everything.
If I were setting conventions again, I would default to a small amount of duplication over a shared library, and reserve libraries for genuinely stable infrastructure. When real logic needs to be shared, I would expose it as a service and share the contract, not the code. Duplication is cheap and local; coupling is expensive and global, and a shared library quietly trades the cheap problem for the expensive one. The closely related hazard of one team’s model leaking into another is covered in Bounded Contexts in Real Microservice Systems.
Sources
- Sam Newman, Building Microservices (DRY and code reuse across services): samnewman.io/books/building_microservices_2nd_edition
- Martin Fowler, Microservices and shared code: martinfowler.com/articles/microservices.html
- Semantic Versioning specification: semver.org
Frequently asked questions
Should microservices share libraries?
Sparingly, and only for stable, generic concerns like serialization, logging format, or protocol clients. Sharing business logic in a library recouples the services that were supposed to be independent, because a change to the library forces every consumer to update. Prefer sharing contracts over sharing code.
Why are shared libraries risky in microservices?
Because a shared library is a hidden coupling. When the library changes, every service that depends on it must eventually adopt the new version, so a single library change can ripple across many teams. It quietly recreates the lockstep-deploy problem microservices were meant to avoid.
What should be in a shared library versus a service?
Put stable, generic, business-agnostic concerns in a shared library (serialization, logging conventions, generated protocol clients, telemetry setup). Keep business logic and anything that changes often behind a service API instead, so consumers call it rather than compiling it in.
How do you version a shared library across many services?
Use semantic versioning, never break compatibility within a major version, and let services upgrade on their own schedule rather than forcing a synchronized bump. Avoid a single shared version everyone must move to at once, which reintroduces lockstep coupling.