One shared TypeScript library for every service, without a god library
At KitchenSync, an accounting-automation platform for restaurants, I designed a shared TypeScript library that every service in the ecosystem used. It grew to more than 16 modules: reports, transactions, budgets, auditing, validation and more.
A shared library like that can save a team a lot of duplicated work. It can also become the one package everyone is afraid to touch. These are the decisions that keep it on the right side.
Why a shared library at all
In an accounting product, the same concepts show up everywhere: what a transaction is, how a budget is calculated, what an audit entry looks like, what counts as valid input. If each service implements those on its own, they drift. Two services computing a report slightly differently is not a style issue in accounting; it's a wrong number.
A shared library gives the platform one definition of those concepts: types, validation and domain logic that every service imports.
Boundaries: modules, not a bag of helpers
The library was organized as modules by domain, each with a clear purpose. A few rules help:
- Each module owns one domain concept. Reports, transactions, budgets, auditing, validation.
- Modules expose a small public surface. Internals stay internal, so they can change.
- Dependencies between modules go one way. If two modules need each other, the boundary is wrong.
- No framework or infrastructure code in the domain modules. Pure types and logic are easy to reuse and easy to test.
What doesn't belong in it
The fastest way to create a god library is to accept everything "because it's shared." I'd keep out:
- Service-specific logic. If only one service uses it, it lives in that service.
- Clients for specific infrastructure mixed into domain code.
- Random utilities. A
utilsfolder that everything depends on is where coupling hides.
A useful question for every addition: would two services disagree if this weren't shared? If not, it probably doesn't need to be shared.
Versioning and change
When every service imports the same package, a change can break all of them at once. That calls for discipline:
- Version the library and let services upgrade deliberately.
- Prefer backwards-compatible changes: add, deprecate, then remove.
- Test the library heavily, because a bug in it is a bug everywhere.
- Review changes with the consumers in mind, not just the library itself.
The team side
A shared library is also a social contract. With a team of engineers depending on it, code review and clear ownership matter as much as code structure. As a lead, part of my job was keeping the library's scope clear and making sure changes were reviewed with everyone who used it in mind.
Where it paid off
Beyond consistency, the library made the rest of the platform easier to build. Validation lived in one place. Auditing looked the same across services. And when we moved per-customer rules into configuration, and later added LLMs, there was already a shared vocabulary for transactions and audit trails to build on. I wrote about those in Rules as data and LLMs in production.
What I'd recommend
- Share domain concepts, not convenience code.
- Organize by domain modules with small public surfaces.
- Keep dependencies one-directional and infrastructure out of the core.
- Version, test and review it like the critical dependency it is.
- Ask before every addition whether sharing it prevents a real disagreement.