XRAPH/Writing/Why almost everything I ship is a library

Why almost everything I ship is a library

Job queues, webhook delivery, authorisation and billing as libraries you compile in rather than services you operate. The reasoning, and the three places it fails.

Published
Feb 2025
Length
2 min read
Systems
5

#A pattern I noticed afterwards

Around the eighth module I realised every README had independently converged on the same sentence. A library, not a service. Import it, compose it, own your infrastructure.

I had not planned that. It came from making the same call repeatedly for reasons that felt local each time.

#What a service costs that a library does not

Every self-hosted service is an operational unit. A deployment, a health check, a network path, credentials, a backup story, a version skew policy against its clients, an upgrade window, and somebody who knows how to restart it at the weekend . That cost is roughly constant regardless of how much of it you use.

For a job queue, a webhook sender or an authorisation check, it frequently exceeds the thing itself. A team of four running a workflow engine as a separate deployment spends more on the engine's operations than on the workflows.

The second cost bit harder. An authorisation check as a remote call means every request depends on another service being reachable, so you build a cache, and the cache has an invalidation bug. In process there is no third thing to be down.

#What you give up

  1. Polyglot access. A Go library is for Go programs. If half your stack is Python this is a hard stop, and the answer is to run it as a service, for which one of these libraries is a reasonable foundation.
  2. Independent scaling. Workers scale with the process they live in. Running the same binary in a worker-only mode gets most of the way there and is a workaround for a real limitation.
  3. Upgrade coupling. Upgrading means redeploying. With a service you can upgrade the engine without touching clients, which sounds like an advantage until the version skew that permits becomes the outage .
Every dependency you can compile in is a dependency that cannot be independently unavailable on a Friday afternoon.

#The discipline it imposes

Each library has a framework extension and also a plain constructor, its own context helpers, and an in-memory store so the quick start runs with no infrastructure. If a library only worked inside the framework, the framework's assumptions would leak into its design and nothing would fail to warn me.

1// Standalone: no framework, no database, no Redis.
2d, _ := dispatch.New(dispatch.WithStore(memory.New()))
3eng := engine.Build(d)
4engine.Register(eng, SendEmail)

#Where the line is

Two of my projects are not libraries. A gateway is a binary, because it is genuinely a separate network hop and pretending otherwise helps nobody. The end-to-end argument is the relevant one here : some functions belong at a boundary because that is the only place they can be enforced completely.

The test I use: if the thing must exist between processes, it is a service. If it must exist inside one, it is a library, and making it a service adds a failure domain for no benefit .

References

  1. [1]Sam Newman, Building Microservices: Designing Fine-Grained Systems, O'Reilly Media, 2nd edition, 2021
  2. [2]Michael T. Nygard, Release It! Design and Deploy Production-Ready Software, Pragmatic Bookshelf, 2nd edition, 2018
  3. [3]J. H. Saltzer, D. P. Reed, D. D. Clark, End-to-End Arguments in System Design, ACM Transactions on Computer Systems, vol. 2, no. 4, pp. 277-288, 1984doi:10.1145/357401.357402
  4. [4]Betsy Beyer, Chris Jones, Jennifer Petoff, Niall Richard Murphy, Site Reliability Engineering: How Google Runs Production Systems, O'Reilly Media, 2016