#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
- 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.
- 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.
- 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]Sam Newman, “Building Microservices: Designing Fine-Grained Systems”, O'Reilly Media, 2nd edition, 2021
- [2]Michael T. Nygard, “Release It! Design and Deploy Production-Ready Software”, Pragmatic Bookshelf, 2nd edition, 2018
- [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]Betsy Beyer, Chris Jones, Jennifer Petoff, Niall Richard Murphy, “Site Reliability Engineering: How Google Runs Production Systems”, O'Reilly Media, 2016